package idv.takeshi.software.productline.bookstore.domain.model.book;


import java.math.BigDecimal;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.net.URL;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.OrderBy;
import javax.persistence.Transient;

import org.apache.commons.lang.LocaleUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import idv.takeshi.software.productline.bookstore.domain.model.useraccount.Publisher;

/**
 * A model class for representing as a book.
 * @author takeshi
 *
 */
@Entity
@NamedQueries(
		{
			@NamedQuery(name = "findByName", query = "SELECT b FROM Book b WHERE b.name = :name"),
			@NamedQuery(name = "findByKeyWord", query = "SELECT DISTINCT b FROM Book b JOIN b.authorRelationships ar JOIN ar.author a, IN(b.publisher) p JOIN p.userAccount ua WHERE UPPER(b.name) LIKE UPPER(:keyWord) OR UPPER(ua.name) LIKE UPPER(:keyWord) OR UPPER(a.name) LIKE UPPER(:keyWord) ORDER BY b.publishDate DESC"),
			@NamedQuery(name = "findAmountByKeyWord", query = "SELECT COUNT(DISTINCT b) FROM Book b JOIN b.authorRelationships ar JOIN ar.author a, IN(b.publisher) p JOIN p.userAccount ua WHERE UPPER(b.name) LIKE UPPER(:keyWord) OR UPPER(ua.name) LIKE UPPER(:keyWord) OR UPPER(a.name) LIKE UPPER(:keyWord)")}	
	)
public class Book {
	
	private static final Log log = LogFactory.getLog(Book.class);
	
	@Id
	@GeneratedValue(strategy = GenerationType.TABLE)
	private Long id;
	
	@Column(nullable = false, length = 200)
	private String name;
	
	@Column(nullable = false, name = "FIXED_PRICE")
	private BigDecimal fixedPrice;
	
	@Column(nullable = false, name = "UNIT_PRICE")
	private BigDecimal unitPrice;

	@Column(nullable = false, name = "PUBLISH_DATE")
	private Date publishDate;
	
	@Column(nullable = false, length = 10)
	private String locale;
	
	@Column(nullable = false, length = 13, unique = true)
	private String isbn;
	
	@Column(length = 13, unique = true)
	private String ean;
	
	@Column(length = 1000)
	private String description;
	
	@Column
	private int pages;
	
	@Column
	private String image;
	
	@OneToOne(fetch=FetchType.EAGER)
	@JoinColumn(name="INVENTORY_ID", nullable = false)
	private Inventory inventory;
	
	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "PUBLISHER_ID", nullable = false)
	private Publisher publisher;
	
	@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
	@JoinColumn(name = "BOOK_ID", nullable = false)
	@OrderBy("sequence ASC")
	private Set<AuthorRelationship> authorRelationships = new LinkedHashSet<AuthorRelationship>();
	
	@OneToOne(fetch = FetchType.EAGER)
	@JoinColumn(name="PROMOTION_ID")
	private Promotion promotion;
	 
	/**
	 * @return the id
	 */
	public Long getId() {
		return id;
	}

	/**
	 * @param id the id to set
	 */
	public void setId(Long id) {
		this.id = id;
	}

	/**
	 * @return the name
	 */
	public String getName() {
		return name;
	}

	/**
	 * @param name the name to set
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * @return the fixedPrice
	 */
	public BigDecimal getFixedPrice() {
		return fixedPrice;
	}

	/**
	 * @param fixedPrice the fixedPrice to set
	 */
	public void setFixedPrice(BigDecimal fixedPrice) {
		this.fixedPrice = fixedPrice;
	}

	/**
	 * @return the publishDate
	 */
	public Date getPublishDate() {
		return publishDate;
	}

	/**
	 * @param publishDate the publishDate to set
	 */
	public void setPublishDate(Date publishDate) {
		this.publishDate = publishDate;
	}

	/**
	 * @return the locale
	 */
	public String getLocale() {
		return locale;
	}

	/**
	 * @param locale the locale to set
	 */
	public void setLocale(String locale) {
		//test whether this locale format is valid
		LocaleUtils.toLocale(locale);
		this.locale = locale;
	}

	/**
	 * @return the isbn
	 */
	public String getIsbn() {
		return isbn;
	}

	/**
	 * @param isbn the isbn to set
	 */
	public void setIsbn(String isbn) {
		this.isbn = isbn;
	}

	/**
	 * @return the ean
	 */
	public String getEan() {
		return ean;
	}

	/**
	 * @param ean the ean to set
	 */
	public void setEan(String ean) {
		this.ean = ean;
	}

	/**
	 * @return the description
	 */
	public String getDescription() {
		return description;
	}

	/**
	 * @param description the description to set
	 */
	public void setDescription(String description) {
		this.description = description;
	}

	/**
	 * @return the pages
	 */
	public int getPages() {
		return pages;
	}

	/**
	 * @param pages the pages to set
	 */
	public void setPages(int pages) {
		this.pages = pages;
	}

	/**
	 * @return the image
	 */
	public String getImage() {
		return image;
	}

	/**
	 * @param image the image to set
	 */
	public void setImage(String image) {
		this.image = image;
	}

	/**
	 * @return the publisher
	 */
	public Publisher getPublisher() {
		return publisher;
	}

	/**
	 * @param publisher the publisher to set
	 */
	public void setPublisher(Publisher publisher) {
		this.publisher = publisher;
	}

	/**
	 * @return the authors
	 */
	public Set<Author> getAuthors() {
		Set<Author> authors = new LinkedHashSet<Author>();
		if(this.authorRelationships.size() > 0){
			for(AuthorRelationship authorRelationship : this.authorRelationships){
				authors.add(authorRelationship.getAuthor());
			}
		}
		return authors;
	}

	/**
	 * @param authors the authors to set
	 */
	public void setAuthors(Set<Author> authors) {
		Validate.notEmpty(authors, "authors shall always not be null/empty");
		AuthorRelationship authorRelationship = null;
		int sequence = 1;
		if(authors.size() > 0){
			this.authorRelationships.clear();
			for(Author author : authors){
				authorRelationship = new AuthorRelationship();
				authorRelationship.setAuthor(author);
				authorRelationship.setSequence(sequence++);
				this.authorRelationships.add(authorRelationship);
			}
		}
	}

	/**
	 * @return the promotion
	 */
	public Promotion getPromotion() {
		return promotion;
	}

	/**
	 * Get Inventory.
	 * @return
	 */
	public Inventory getInventory() {
		return this.inventory;
	}

	/**
	 * @param inventory the inventory to set
	 */
	protected void setInventory(Inventory inventory) {
		this.inventory = inventory;
	}

	/**
	 * Set a promotion for this book.
	 * @param <T>
	 * @param clazz
	 * @return A promotion for this book.
	 */
	public <T extends Promotion> T setPromotion(Class<T> clazz) {
		Validate.notNull(clazz, "clazz shall always not be null");
		T promotion = null;
		try {
			promotion = clazz.newInstance();
			this.promotion = promotion;
		} catch (Exception e) {
			String msg = "new instance of " + clazz + " error";
			log.error(msg, e);
			throw new RuntimeException(msg, e);
		}
		return promotion;
	}
	
	/**
	 * Get unitPrice for this book.
	 * UnitPrice might be calculated through the set promotion of this book,
	 * otherwise the unitPrice would be the same with fixedprice.
	 * @return
	 */
	public BigDecimal getUnitPrice(){
		if(null == this.unitPrice){
			this.calculateUnitPrice();
		}
		return unitPrice;
	}
	
	/**
	 * Calculate Unit Price for this book.
	 */
	public void calculateUnitPrice(){
		if(null != promotion){
			unitPrice = promotion.getPromotedPrice(this);
		} else{
			unitPrice = this.getFixedPrice();
		}
	}
	 
	/**
	 * Remove attached promotion.
	 */
	public void removePromotion() {
		this.promotion = null;
	}
	 
	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj == null) { return false; }
		   if (obj == this) { return true; }
		   if (!(obj instanceof Book)) {
		     return false;
		   }
		   Book rhs = (Book) obj;
		   return new EqualsBuilder()
		       .append(this.id, rhs.id)
			   .append(this.name, rhs.name)
			   .append(this.fixedPrice, rhs.fixedPrice)
			   .append(this.publishDate, rhs.publishDate)
			   .append(this.locale, rhs.locale)
			   .append(this.isbn, rhs.isbn)
			   .append(this.ean, rhs.ean)
			   .append(this.description, rhs.description)
			   .append(this.pages, rhs.pages)
			   .append(this.image, rhs.image)
			   .append(this.inventory, rhs.inventory)
			   .append(this.publisher, rhs.publisher)
			   .append(this.authorRelationships, rhs.authorRelationships)
			   .append(this.promotion, rhs.promotion)
		       .isEquals();
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return new HashCodeBuilder(5, 73).
		   append(this.id).
	       append(this.name).
	       append(this.fixedPrice).
	       append(this.publishDate).
	       append(this.locale).
	       append(this.isbn).
	       append(this.ean).
	       append(this.description).
	       append(this.pages).
	       append(this.image).
	       append(this.inventory).
	       append(this.publisher).
	       append(this.authorRelationships).
	       append(this.promotion).
	       toHashCode();
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return ReflectionToStringBuilder.toString(this, ToStringStyle.MULTI_LINE_STYLE);
	}

	 
}
 
